use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

use parser::node::Whitespace;
use parser::{Node, Parsed};
use proc_macro2::Span;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta, Token};

use crate::config::{Config, SyntaxAndCache};
use crate::spans::SourceSpan;
use crate::{CompileError, FileInfo, HashMap, MsgValidEscapers};

#[derive(Clone)]
pub(crate) struct TemplateInput<'a> {
    pub(crate) template_span: Span,
    pub(crate) ast: &'a syn::DeriveInput,
    pub(crate) enum_ast: Option<&'a syn::DeriveInput>,
    pub(crate) config: &'a Config,
    pub(crate) syntax: &'a SyntaxAndCache<'a>,
    pub(crate) source: &'a Source,
    pub(crate) source_span: SourceSpan,
    pub(crate) block: Option<(&'a str, Span)>,
    #[cfg(feature = "blocks")]
    pub(crate) blocks: &'a [Block],
    pub(crate) print: Print,
    pub(crate) escaper: &'a str,
    pub(crate) path: Arc<Path>,
    pub(crate) fields: Arc<[String]>,
}

impl TemplateInput<'_> {
    /// Extract the template metadata from the `DeriveInput` structure. This
    /// mostly recovers the data for the `TemplateInput` fields from the
    /// `template()` attribute list fields.
    pub(crate) fn new<'n>(
        ast: &'n syn::DeriveInput,
        enum_ast: Option<&'n syn::DeriveInput>,
        config: &'n Config,
        args: &'n TemplateArgs,
    ) -> Result<TemplateInput<'n>, CompileError> {
        let TemplateArgs {
            template_span,
            source: (source, source_span),
            block,
            #[cfg(feature = "blocks")]
            blocks,
            print,
            escaping,
            ext,
            ext_span,
            syntax,
            ..
        } = args;

        // Validate the `source` and `ext` value together, since they are
        // related. In case `source` was used instead of `path`, the value
        // of `ext` is merged into a synthetic `path` value here.
        let path: Arc<Path> = match (&source, &ext) {
            #[cfg(feature = "external-sources")]
            (Source::Path(path), _) => {
                config.find_template(path, None, None, Some(source_span.config_span()))?
            }
            (&Source::Source(_), Some(ext)) => {
                PathBuf::from(format!("{}.{}", ast.ident, ext)).into()
            }
            (&Source::Source(_), None) => {
                return Err(CompileError::no_file_info(
                    #[cfg(not(feature = "code-in-doc"))]
                    "must include `ext` attribute when using `source` attribute",
                    #[cfg(feature = "code-in-doc")]
                    "must include `ext` attribute when using `source` or `in_doc` attribute",
                    None,
                ));
            }
        };

        // Validate syntax
        let syntax = syntax.as_deref().map_or_else(
            || Ok(config.syntaxes.get(config.default_syntax).unwrap()),
            |s| {
                config.syntaxes.get(s).ok_or_else(|| {
                    CompileError::no_file_info(format_args!("syntax `{s}` is undefined"), None)
                })
            },
        )?;

        // Match extension against defined output formats

        let escaping = escaping
            .as_deref()
            .or_else(|| path.extension().and_then(|s| s.to_str()))
            .unwrap_or_default();

        let escaper = config
            .escapers
            .iter()
            .find_map(|(extensions, path)| {
                extensions
                    .contains(&Cow::Borrowed(escaping))
                    .then_some(path.as_ref())
            })
            .ok_or_else(|| {
                CompileError::no_file_info(
                    format_args!(
                        "no escaper defined for extension '{escaping}'. You can define an escaper \
                        in the config file (named `askama.toml` by default). {}",
                        MsgValidEscapers(&config.escapers),
                    ),
                    *ext_span,
                )
            })?;

        let empty_punctuated = Punctuated::new();
        let fields = match ast.data {
            syn::Data::Struct(ref struct_) => {
                if let syn::Fields::Named(fields) = &struct_.fields {
                    &fields.named
                } else {
                    &empty_punctuated
                }
            }
            syn::Data::Union(ref union_) => &union_.fields.named,
            syn::Data::Enum(_) => &empty_punctuated,
        }
        .iter()
        .map(|f| match &f.ident {
            Some(ident) => ident.to_string(),
            None => unreachable!("we checked that we are using a struct"),
        })
        .collect::<Vec<_>>();

        Ok(TemplateInput {
            template_span: *template_span,
            ast,
            enum_ast,
            config,
            syntax,
            source,
            source_span: source_span.clone(),
            block: block.as_ref().map(|(block, span)| (block.as_str(), *span)),
            #[cfg(feature = "blocks")]
            blocks: blocks.as_slice(),
            print: *print,
            escaper,
            path,
            fields: fields.into(),
        })
    }

    pub(crate) fn find_used_templates(
        &self,
        map: &mut HashMap<Arc<Path>, Arc<Parsed>>,
    ) -> Result<(), CompileError> {
        let (source, source_path) = match &self.source {
            Source::Source(s) => (s.clone(), None),
            #[cfg(feature = "external-sources")]
            Source::Path(_) => (
                get_template_source(&self.path, None)?,
                Some(Arc::clone(&self.path)),
            ),
        };

        #[cfg(feature = "external-sources")]
        let mut dependency_graph = Vec::new();

        let mut check = vec![(Arc::clone(&self.path), source, source_path)];
        while let Some((path, source, source_path)) = check.pop() {
            let parsed = match self.syntax.parse(Arc::clone(&source), source_path) {
                Ok(parsed) => parsed,
                Err(err) => {
                    let msg = err
                        .message
                        .unwrap_or_else(|| "failed to parse template source".into());
                    let file_path = err
                        .file_path
                        .as_deref()
                        .unwrap_or(Path::new("<source attribute>"));
                    let file_info =
                        FileInfo::new(file_path, Some(&source), Some(&source[err.offset..]));
                    return Err(CompileError::new(msg, Some(file_info)));
                }
            };

            let mut top = true;
            let mut nested = vec![parsed.nodes()];
            while let Some(nodes) = nested.pop() {
                for n in nodes {
                    #[cfg(feature = "external-sources")]
                    let mut add_to_check = |new_path: Arc<Path>| -> Result<(), CompileError> {
                        if let std::collections::hash_map::Entry::Vacant(e) = map.entry(new_path) {
                            // Add a dummy entry to `map` in order to prevent adding `path`
                            // multiple times to `check`.
                            let new_path = e.key();
                            let source = parsed.source();
                            let source = get_template_source(
                                new_path,
                                Some((
                                    &path,
                                    source,
                                    n.span().as_suffix_of(source).unwrap_or_default(),
                                )),
                            )?;
                            check.push((new_path.clone(), source, Some(new_path.clone())));
                        }
                        Ok(())
                    };

                    match &**n {
                        Node::Extends(extends) if top => {
                            #[cfg(not(feature = "external-sources"))]
                            {
                                return node_needs_external_sources(
                                    "extends",
                                    extends.span(),
                                    &path,
                                    &parsed,
                                );
                            }
                            #[cfg(feature = "external-sources")]
                            {
                                let extends = self.config.find_template(
                                    extends.path,
                                    Some(&path),
                                    Some(FileInfo::of(extends.span(), &path, &parsed)),
                                    Some(self.source_span.config_span()),
                                )?;
                                let dependency_path = (path.clone(), extends.clone());
                                if path == extends {
                                    // We add the path into the graph to have a better looking error.
                                    dependency_graph.push(dependency_path);
                                    return cyclic_graph_error(&dependency_graph);
                                } else if dependency_graph.contains(&dependency_path) {
                                    return cyclic_graph_error(&dependency_graph);
                                }
                                dependency_graph.push(dependency_path);
                                add_to_check(extends)?;
                            }
                        }
                        Node::Macro(m) if top => {
                            nested.push(&m.nodes);
                        }
                        Node::Import(import) if top => {
                            #[cfg(not(feature = "external-sources"))]
                            {
                                return node_needs_external_sources(
                                    "import",
                                    import.span(),
                                    &path,
                                    &parsed,
                                );
                            }
                            #[cfg(feature = "external-sources")]
                            {
                                let import = self.config.find_template(
                                    import.path,
                                    Some(&path),
                                    Some(FileInfo::of(import.span(), &path, &parsed)),
                                    Some(self.source_span.config_span()),
                                )?;
                                add_to_check(import)?;
                            }
                        }
                        Node::FilterBlock(f) => {
                            nested.push(&f.nodes);
                        }
                        Node::Include(include) => {
                            #[cfg(not(feature = "external-sources"))]
                            {
                                return node_needs_external_sources(
                                    "include",
                                    include.span(),
                                    &path,
                                    &parsed,
                                );
                            }
                            #[cfg(feature = "external-sources")]
                            {
                                let include = self.config.find_template(
                                    include.path,
                                    Some(&path),
                                    Some(FileInfo::of(include.span(), &path, &parsed)),
                                    Some(self.source_span.config_span()),
                                )?;
                                add_to_check(include)?;
                            }
                        }
                        Node::BlockDef(b) => {
                            nested.push(&b.nodes);
                        }
                        Node::If(i) => {
                            for cond in &i.branches {
                                nested.push(&cond.nodes);
                            }
                        }
                        Node::Loop(l) => {
                            nested.push(&l.body);
                            nested.push(&l.else_nodes);
                        }
                        Node::Match(m) => {
                            for arm in &m.arms {
                                nested.push(&arm.nodes);
                            }
                        }
                        Node::Call(c) => {
                            nested.push(&c.nodes);
                        }
                        Node::Lit(_)
                        | Node::Comment(_)
                        | Node::Expr(_, _)
                        | Node::Extends(_)
                        | Node::Let(_)
                        | Node::Import(_)
                        | Node::Macro(_)
                        | Node::Raw(_)
                        | Node::Continue(_)
                        | Node::Break(_) => {}
                    }
                }
                top = false;
            }
            map.insert(path, parsed);
        }
        Ok(())
    }
}

#[cfg(not(feature = "external-sources"))]
fn node_needs_external_sources(
    kind: &str,
    node: parser::Span,
    path: &Path,
    parsed: &Parsed,
) -> Result<(), CompileError> {
    Err(CompileError::new(
        format!("enable feature `external-sources` to use `{{% {kind} %}}`"),
        Some(FileInfo::of(node, path, parsed)),
    ))
}

pub(crate) enum AnyTemplateArgs {
    Struct(TemplateArgs),
    Enum {
        enum_args: Option<PartialTemplateArgs>,
        vars_args: Vec<Option<PartialTemplateArgs>>,
        has_default_impl: bool,
    },
}

impl AnyTemplateArgs {
    pub(crate) fn new(ast: &syn::DeriveInput) -> Result<Self, CompileError> {
        let syn::Data::Enum(enum_data) = &ast.data else {
            return Ok(Self::Struct(TemplateArgs::new(ast)?));
        };

        let enum_args = PartialTemplateArgs::new(ast, &ast.attrs, false)?;
        let vars_args = enum_data
            .variants
            .iter()
            .map(|variant| PartialTemplateArgs::new(ast, &variant.attrs, true))
            .collect::<Result<Vec<_>, _>>()?;
        if vars_args.is_empty() {
            return Ok(Self::Struct(TemplateArgs::from_partial(ast, enum_args)?));
        }

        let mut needs_default_impl = vars_args.len();
        let enum_source = enum_args.as_ref().and_then(|v| v.source.as_ref());
        for (variant, var_args) in enum_data.variants.iter().zip(&vars_args) {
            if var_args
                .as_ref()
                .and_then(|v| v.source.as_ref())
                .or(enum_source)
                .is_none()
            {
                return Err(CompileError::new_with_span(
                    #[cfg(not(feature = "code-in-doc"))]
                    "either all `enum` variants need a `path` or `source` argument, \
                    or the `enum` itself needs a default implementation",
                    #[cfg(feature = "code-in-doc")]
                    "either all `enum` variants need a `path`, `source` or `in_doc` argument, \
                    or the `enum` itself needs a default implementation",
                    None,
                    Some(variant.ident.span()),
                ));
            } else if !var_args.is_none() {
                needs_default_impl -= 1;
            }
        }

        Ok(Self::Enum {
            enum_args,
            vars_args,
            has_default_impl: needs_default_impl > 0,
        })
    }

    pub(crate) fn take_crate_name(&mut self) -> Option<ExprPath> {
        match self {
            AnyTemplateArgs::Struct(template_args) => template_args.crate_name.take(),
            AnyTemplateArgs::Enum { enum_args, .. } => {
                if let Some(PartialTemplateArgs { crate_name, .. }) = enum_args {
                    crate_name.take()
                } else {
                    None
                }
            }
        }
    }
}

#[cfg(feature = "blocks")]
pub(crate) struct Block {
    pub(crate) name: String,
    pub(crate) span: Span,
}

pub(crate) struct TemplateArgs {
    template_span: Span,
    pub(crate) source: (Source, SourceSpan),
    block: Option<(String, Span)>,
    #[cfg(feature = "blocks")]
    blocks: Vec<Block>,
    print: Print,
    escaping: Option<String>,
    ext: Option<String>,
    ext_span: Option<Span>,
    syntax: Option<String>,
    config: Option<String>,
    crate_name: Option<ExprPath>,
    pub(crate) whitespace: Option<Whitespace>,
    pub(crate) config_span: Option<Span>,
}

impl TemplateArgs {
    pub(crate) fn new(ast: &syn::DeriveInput) -> Result<Self, CompileError> {
        Self::from_partial(ast, PartialTemplateArgs::new(ast, &ast.attrs, false)?)
    }

    pub(crate) fn from_partial(
        ast: &syn::DeriveInput,
        args: Option<PartialTemplateArgs>,
    ) -> Result<Self, CompileError> {
        let Some(args) = args else {
            return Err(CompileError::new_with_span(
                "no attribute `template` found",
                None,
                Some(ast.ident.span()),
            ));
        };
        Ok(Self {
            template_span: args.template_span,
            source: match args.source {
                #[cfg(feature = "external-sources")]
                Some(PartialTemplateArgsSource::Path(s)) => {
                    (Source::Path(s.value().into()), SourceSpan::from_path(s)?)
                }
                Some(PartialTemplateArgsSource::Source(s)) => {
                    let (source, span) = SourceSpan::from_source(s)?;
                    (Source::Source(source.into()), span)
                }
                #[cfg(feature = "code-in-doc")]
                Some(PartialTemplateArgsSource::InDoc(span, source)) => {
                    (source, SourceSpan::CodeInDoc(span))
                }
                None => {
                    return Err(CompileError::no_file_info(
                        #[cfg(not(feature = "code-in-doc"))]
                        "specify one template argument `path` or `source`",
                        #[cfg(feature = "code-in-doc")]
                        "specify one template argument `path`, `source` or `in_doc`",
                        Some(args.template_span),
                    ));
                }
            },
            block: args.block.map(|value| (value.value(), value.span())),
            #[cfg(feature = "blocks")]
            blocks: args
                .blocks
                .unwrap_or_default()
                .into_iter()
                .map(|value| Block {
                    name: value.value(),
                    span: value.span(),
                })
                .collect(),
            print: args.print.unwrap_or_default(),
            escaping: args.escape.map(|value| value.value()),
            ext: args.ext.as_ref().map(|value| value.value()),
            ext_span: args.ext.as_ref().map(|value| value.span()),
            syntax: args.syntax.map(|value| value.value()),
            config: args.config.as_ref().map(|value| value.value()),
            crate_name: args.crate_name,
            whitespace: args.whitespace,
            config_span: args.config.as_ref().map(|value| value.span()),
        })
    }

    pub(crate) fn fallback() -> Self {
        Self {
            template_span: Span::call_site(),
            source: (Source::Source("".into()), SourceSpan::empty()),
            block: None,
            #[cfg(feature = "blocks")]
            blocks: vec![],
            print: Print::default(),
            escaping: None,
            ext: Some("txt".to_string()),
            ext_span: None,
            syntax: None,
            config: None,
            crate_name: None,
            whitespace: None,
            config_span: None,
        }
    }

    pub(crate) fn config_path(&self) -> Option<&str> {
        self.config.as_deref()
    }
}

/// Try to find the source in the comment, in a `askama` code block.
///
/// This is only done if no path or source was given in the `#[template]` attribute.
#[cfg(feature = "code-in-doc")]
fn source_from_docs(
    span: Span,
    docs: &[&Attribute],
    ast: &syn::DeriveInput,
) -> Result<(Source, Option<Span>), CompileError> {
    let (source_span, source) = collect_comment_blocks(span, docs, ast)?;
    let source = strip_common_ws_prefix(source);
    let source = collect_askama_code_blocks(span, ast, source)?;
    Ok((source, source_span))
}

#[cfg(feature = "code-in-doc")]
fn collect_comment_blocks(
    span: Span,
    docs: &[&Attribute],
    ast: &syn::DeriveInput,
) -> Result<(Option<Span>, String), CompileError> {
    let mut source_span: Option<Span> = None;
    let mut assign_span = |kv: &syn::MetaNameValue| {
        // FIXME: uncomment once <https://github.com/rust-lang/rust/issues/54725> is stable
        // let new_span = kv.path.span();
        // source_span = Some(match source_span {
        //     Some(cur_span) => cur_span.join(new_span).unwrap_or(cur_span),
        //     None => new_span,
        // });

        if source_span.is_none() {
            source_span = Some(kv.path.span());
        }
    };

    let mut source = String::new();
    for a in docs {
        // is a comment?
        let Meta::NameValue(kv) = &a.meta else {
            continue;
        };
        if !kv.path.is_ident("doc") {
            continue;
        }

        // is an understood comment, e.g. not `#[doc = inline_str(…)]`
        let mut value = &kv.value;
        let value = loop {
            match value {
                Expr::Lit(lit) => break lit,
                Expr::Group(group) => value = &group.expr,
                _ => continue,
            }
        };
        let Lit::Str(value) = &value.lit else {
            continue;
        };

        assign_span(kv);
        source.push_str(value.value().as_str());
        source.push('\n');
    }
    if source.is_empty() {
        return Err(no_askama_code_block(span, ast));
    }

    Ok((source_span, source))
}

#[cfg(feature = "code-in-doc")]
fn no_askama_code_block(span: Span, ast: &syn::DeriveInput) -> CompileError {
    let kind = match &ast.data {
        syn::Data::Struct(_) => "struct",
        syn::Data::Enum(_) => "enum",
        // actually unreachable: `union`s are rejected by `TemplateArgs::new()`
        syn::Data::Union(_) => "union",
    };
    CompileError::no_file_info(
        format_args!(
            "when using `in_doc` with the value `true`, the {kind}'s documentation needs a \
             `askama` code block"
        ),
        Some(span),
    )
}

#[cfg(feature = "code-in-doc")]
fn strip_common_ws_prefix(source: String) -> String {
    let mut common_prefix_iter = source
        .lines()
        .filter_map(|s| Some(&s[..s.find(|c: char| !c.is_ascii_whitespace())?]));
    let mut common_prefix = common_prefix_iter.next().unwrap_or_default();
    for p in common_prefix_iter {
        if common_prefix.is_empty() {
            break;
        }
        let ((pos, _), _) = common_prefix
            .char_indices()
            .zip(p.char_indices())
            .take_while(|(l, r)| l == r)
            .last()
            .unwrap_or_default();
        common_prefix = &common_prefix[..pos];
    }
    if common_prefix.is_empty() {
        return source;
    }

    source
        .lines()
        .flat_map(|s| [s.get(common_prefix.len()..).unwrap_or_default(), "\n"])
        .collect()
}

#[cfg(feature = "code-in-doc")]
fn collect_askama_code_blocks(
    span: Span,
    ast: &syn::DeriveInput,
    source: String,
) -> Result<Source, CompileError> {
    use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};

    let mut tmpl_source = String::new();
    let mut in_askama_code = false;
    let mut had_askama_code = false;
    for e in Parser::new(&source) {
        match (in_askama_code, e) {
            (false, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(s)))) => {
                if s.split(",")
                    .any(|s| JINJA_EXTENSIONS.contains(&s.trim_ascii()))
                {
                    in_askama_code = true;
                    had_askama_code = true;
                }
            }
            (true, Event::End(TagEnd::CodeBlock)) => in_askama_code = false,
            (true, Event::Text(text)) => tmpl_source.push_str(&text),
            _ => {}
        }
    }
    if !had_askama_code {
        return Err(no_askama_code_block(span, ast));
    }

    if tmpl_source.ends_with('\n') {
        tmpl_source.pop();
    }
    Ok(Source::Source(tmpl_source.into()))
}

#[derive(Debug, Clone, Hash, PartialEq)]
pub(crate) enum Source {
    #[cfg(feature = "external-sources")]
    Path(Arc<str>),
    Source(Arc<str>),
}

#[derive(Clone, Copy, Debug, PartialEq, Hash)]
pub(crate) enum Print {
    All,
    Ast,
    Code,
    None,
}

impl Default for Print {
    fn default() -> Self {
        Self::None
    }
}

impl FromStr for Print {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "all" => Ok(Self::All),
            "ast" => Ok(Self::Ast),
            "code" => Ok(Self::Code),
            "none" => Ok(Self::None),
            _ => Err(format!("invalid value for `print` option: {s}")),
        }
    }
}

#[cfg(feature = "external-sources")]
fn cyclic_graph_error(dependency_graph: &[(Arc<Path>, Arc<Path>)]) -> Result<(), CompileError> {
    Err(CompileError::no_file_info(
        format_args!(
            "cyclic dependency in graph {:#?}",
            dependency_graph
                .iter()
                .map(|e| format!("{:#?} --> {:#?}", e.0, e.1))
                .collect::<Vec<String>>()
        ),
        None,
    ))
}

#[cfg(feature = "external-sources")]
pub(crate) fn get_template_source(
    tpl_path: &Arc<Path>,
    import_from: Option<(&Arc<Path>, &str, &str)>,
) -> Result<Arc<str>, CompileError> {
    static CACHE: std::sync::OnceLock<crate::OnceMap<Arc<Path>, Arc<str>>> =
        std::sync::OnceLock::new();

    CACHE
        .get_or_init(crate::OnceMap::default)
        .get_or_try_insert(
            tpl_path,
            |tpl_path| match std::fs::read_to_string(tpl_path) {
                Ok(mut source) => {
                    if source.ends_with('\n') {
                        let _ = source.pop();
                    }
                    Ok((Arc::clone(tpl_path), Arc::from(source)))
                }
                Err(err) => Err(CompileError::new(
                    format_args!(
                        "unable to open template file '{}': {err}",
                        tpl_path.to_str().unwrap(),
                    ),
                    import_from.map(|(node_file, file_source, node_source)| {
                        FileInfo::new(node_file, Some(file_source), Some(node_source))
                    }),
                )),
            },
            Arc::clone,
        )
}

pub(crate) struct PartialTemplateArgs {
    pub(crate) template_span: Span,
    pub(crate) source: Option<PartialTemplateArgsSource>,
    pub(crate) block: Option<LitStr>,
    pub(crate) print: Option<Print>,
    pub(crate) escape: Option<LitStr>,
    pub(crate) ext: Option<LitStr>,
    pub(crate) syntax: Option<LitStr>,
    pub(crate) config: Option<LitStr>,
    pub(crate) whitespace: Option<Whitespace>,
    pub(crate) crate_name: Option<ExprPath>,
    #[cfg(feature = "blocks")]
    pub(crate) blocks: Option<Vec<LitStr>>,
}

#[derive(Clone)]
pub(crate) enum PartialTemplateArgsSource {
    #[cfg(feature = "external-sources")]
    Path(LitStr),
    Source(LitStr),
    #[cfg(feature = "code-in-doc")]
    InDoc(Span, Source),
}

impl PartialTemplateArgsSource {
    pub(crate) fn span(&self) -> Span {
        match self {
            #[cfg(feature = "external-sources")]
            Self::Path(s) => s.span(),
            Self::Source(s) => s.span(),
            #[cfg(feature = "code-in-doc")]
            Self::InDoc(s, _) => s.span(),
        }
    }
}

// implement PartialTemplateArgs::new()
const _: () = {
    impl PartialTemplateArgs {
        pub(crate) fn new(
            ast: &syn::DeriveInput,
            attrs: &[Attribute],
            is_enum_variant: bool,
        ) -> Result<Option<Self>, CompileError> {
            new(ast, attrs, is_enum_variant)
        }
    }

    #[inline]
    fn new(
        ast: &syn::DeriveInput,
        attrs: &[Attribute],
        is_enum_variant: bool,
    ) -> Result<Option<PartialTemplateArgs>, CompileError> {
        // FIXME: implement once <https://github.com/rust-lang/rfcs/pull/3715> is stable
        if let syn::Data::Union(data) = &ast.data {
            return Err(CompileError::new_with_span_stable(
                "askama templates are not supported for `union` types, only `struct` and `enum`",
                None,
                Some(data.union_token.span),
            ));
        }

        #[cfg(feature = "code-in-doc")]
        let mut meta_docs = vec![];

        let mut this = PartialTemplateArgs {
            template_span: Span::call_site(),
            source: None,
            block: None,
            print: None,
            escape: None,
            ext: None,
            syntax: None,
            config: None,
            whitespace: None,
            crate_name: None,
            #[cfg(feature = "blocks")]
            blocks: None,
        };
        let mut has_data = false;

        for attr in attrs {
            let Some(ident) = attr.path().get_ident() else {
                continue;
            };
            if ident == "template" {
                this.template_span = ident.span();
                has_data = true;
            } else {
                #[cfg(feature = "code-in-doc")]
                if ident == "doc" {
                    meta_docs.push(attr);
                }
                continue;
            }

            let args = attr
                .parse_args_with(<Punctuated<Meta, Token![,]>>::parse_terminated)
                .map_err(|e| {
                    CompileError::no_file_info(
                        format_args!("unable to parse template arguments: {e}"),
                        Some(e.span()),
                    )
                })?;
            for arg in args {
                let pair = match arg {
                    Meta::NameValue(pair) => pair,
                    v => {
                        return Err(CompileError::no_file_info(
                            "unsupported attribute argument",
                            Some(v.span()),
                        ));
                    }
                };
                let ident = match pair.path.get_ident() {
                    Some(ident) => ident,
                    None => unreachable!("not possible in syn::Meta::NameValue(…)"),
                };

                if ident == "askama" {
                    if is_enum_variant {
                        return Err(CompileError::no_file_info(
                            "template attribute `askama` can only be used on the `enum`, \
                            not its variants",
                            Some(ident.span()),
                        ));
                    }
                    ensure_only_once(ident, &mut this.crate_name)?;
                    this.crate_name = Some(get_exprpath(ident, pair.value)?);
                    continue;
                } else if ident == "blocks" {
                    if !cfg!(feature = "blocks") {
                        return Err(CompileError::no_file_info(
                            "enable feature `blocks` to use `blocks` argument",
                            Some(ident.span()),
                        ));
                    } else if is_enum_variant {
                        return Err(CompileError::no_file_info(
                            "template attribute `blocks` can only be used on the `enum`, \
                            not its variants",
                            Some(ident.span()),
                        ));
                    }
                    #[cfg(feature = "blocks")]
                    {
                        ensure_only_once(ident, &mut this.blocks)?;
                        this.blocks = Some(
                            get_exprarray(ident, pair.value)?
                                .elems
                                .into_iter()
                                .map(|value| get_strlit(ident, get_lit(ident, value)?))
                                .collect::<Result<_, _>>()?,
                        );
                        continue;
                    }
                }

                let value = get_lit(ident, pair.value)?;

                if ident == "path" {
                    ensure_source_only_once(ident, &this.source)?;

                    #[cfg(not(feature = "external-sources"))]
                    {
                        return Err(CompileError::no_file_info(
                            "enable feature `external-sources` to use `path` argument",
                            Some(ident.span()),
                        ));
                    }
                    #[cfg(feature = "external-sources")]
                    {
                        this.source =
                            Some(PartialTemplateArgsSource::Path(get_strlit(ident, value)?));
                    }
                } else if ident == "source" {
                    ensure_source_only_once(ident, &this.source)?;
                    this.source =
                        Some(PartialTemplateArgsSource::Source(get_strlit(ident, value)?));
                } else if ident == "in_doc" {
                    let value = get_boollit(ident, value)?;
                    if !value.value() {
                        continue;
                    }
                    ensure_source_only_once(ident, &this.source)?;

                    #[cfg(not(feature = "code-in-doc"))]
                    {
                        return Err(CompileError::no_file_info(
                            "enable feature `code-in-doc` to use `in_doc` argument",
                            Some(ident.span()),
                        ));
                    }
                    #[cfg(feature = "code-in-doc")]
                    {
                        this.source = Some(PartialTemplateArgsSource::InDoc(
                            value.span(),
                            Source::Source("".into()),
                        ));
                    }
                } else if ident == "block" {
                    set_strlit_pair(ident, value, &mut this.block)?;
                } else if ident == "print" {
                    set_parseable_string(ident, value, &mut this.print)?;
                } else if ident == "escape" {
                    set_strlit_pair(ident, value, &mut this.escape)?;
                } else if ident == "ext" {
                    set_strlit_pair(ident, value, &mut this.ext)?;
                } else if ident == "syntax" {
                    set_strlit_pair(ident, value, &mut this.syntax)?;
                } else if ident == "config" {
                    set_strlit_pair(ident, value, &mut this.config)?;
                } else if ident == "whitespace" {
                    set_parseable_string(ident, value, &mut this.whitespace)?;
                } else {
                    return Err(CompileError::no_file_info(
                        format_args!("unsupported template attribute `{ident}` found"),
                        Some(ident.span()),
                    ));
                }
            }
        }
        if !has_data {
            return Ok(None);
        }

        #[cfg(feature = "code-in-doc")]
        if let Some(PartialTemplateArgsSource::InDoc(lit_span, _)) = this.source {
            let (source, doc_span) = source_from_docs(lit_span, &meta_docs, ast)?;
            this.source = Some(PartialTemplateArgsSource::InDoc(
                doc_span.unwrap_or(lit_span),
                source,
            ));
        }

        Ok(Some(this))
    }

    fn set_strlit_pair(
        name: &Ident,
        value: ExprLit,
        dest: &mut Option<LitStr>,
    ) -> Result<(), CompileError> {
        ensure_only_once(name, dest)?;
        *dest = Some(get_strlit(name, value)?);
        Ok(())
    }

    fn set_parseable_string<T: FromStr<Err: ToString>>(
        name: &Ident,
        value: ExprLit,
        dest: &mut Option<T>,
    ) -> Result<(), CompileError> {
        ensure_only_once(name, dest)?;
        let str_value = get_strlit(name, value)?;
        *dest = Some(
            str_value
                .value()
                .parse()
                .map_err(|msg| CompileError::no_file_info(msg, Some(str_value.span())))?,
        );
        Ok(())
    }

    fn ensure_only_once<T>(name: &Ident, dest: &mut Option<T>) -> Result<(), CompileError> {
        if dest.is_none() {
            Ok(())
        } else {
            Err(CompileError::no_file_info(
                format_args!("template attribute `{name}` already set"),
                Some(name.span()),
            ))
        }
    }

    fn get_lit(name: &Ident, mut expr: Expr) -> Result<ExprLit, CompileError> {
        loop {
            match expr {
                Expr::Lit(lit) => return Ok(lit),
                Expr::Group(group) => expr = *group.expr,
                v => {
                    return Err(CompileError::no_file_info(
                        format_args!("template attribute `{name}` expects a literal"),
                        Some(v.span()),
                    ));
                }
            }
        }
    }

    fn get_strlit(name: &Ident, value: ExprLit) -> Result<LitStr, CompileError> {
        if let Lit::Str(s) = value.lit {
            Ok(s)
        } else {
            Err(CompileError::no_file_info(
                format_args!("template attribute `{name}` expects a string literal"),
                Some(value.lit.span()),
            ))
        }
    }

    fn get_boollit(name: &Ident, value: ExprLit) -> Result<LitBool, CompileError> {
        if let Lit::Bool(s) = value.lit {
            Ok(s)
        } else {
            Err(CompileError::no_file_info(
                format_args!("template attribute `{name}` expects a boolean value"),
                Some(value.lit.span()),
            ))
        }
    }

    fn get_exprpath(name: &Ident, mut expr: Expr) -> Result<ExprPath, CompileError> {
        loop {
            match expr {
                Expr::Path(path) => return Ok(path),
                Expr::Group(group) => expr = *group.expr,
                v => {
                    return Err(CompileError::no_file_info(
                        format_args!("template attribute `{name}` expects a path or identifier"),
                        Some(v.span()),
                    ));
                }
            }
        }
    }

    #[cfg(feature = "blocks")]
    fn get_exprarray(name: &Ident, mut expr: Expr) -> Result<syn::ExprArray, CompileError> {
        loop {
            match expr {
                Expr::Array(array) => return Ok(array),
                Expr::Group(group) => expr = *group.expr,
                v => {
                    return Err(CompileError::no_file_info(
                        format_args!("template attribute `{name}` expects an array"),
                        Some(v.span()),
                    ));
                }
            }
        }
    }

    fn ensure_source_only_once(
        name: &Ident,
        source: &Option<PartialTemplateArgsSource>,
    ) -> Result<(), CompileError> {
        if source.is_some() {
            return Err(CompileError::no_file_info(
                #[cfg(feature = "code-in-doc")]
                "must specify `source`, `path` or `is_doc` exactly once",
                #[cfg(not(feature = "code-in-doc"))]
                "must specify `source` or `path` exactly once",
                Some(name.span()),
            ));
        }
        Ok(())
    }
};

#[cfg(feature = "code-in-doc")]
const JINJA_EXTENSIONS: &[&str] = &["askama", "j2", "jinja", "jinja2", "rinja"];

#[test]
#[cfg(feature = "external-sources")]
fn get_source() {
    let path = Config::new("", None, None, None, None)
        .and_then(|config| config.find_template("b.html", None, None, None))
        .unwrap();
    assert_eq!(get_template_source(&path, None).unwrap(), "bar".into());
}
